﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Paddings;

namespace LockCrypt.Core {
    public abstract class EncryptionBase : IEncryptionService {
        /// <summary>
        /// Gets the encryption type.
        /// </summary>
        /// <value>The encryption type.</value>
        public virtual EncryptionProviders EncryptionAlgorithm {
            get { return EncryptionProviders.None; }
        }

        /// <summary>
        /// Gets or sets the encryption key.
        /// </summary>
        /// <value>The encryption key.</value>
        public virtual string EncryptionKey { get; set; }

        public virtual PaddingMode PaddingMode { get; protected set; }
        public virtual CipherMode CipherMode { get; protected set; }

        #region Method Stubs
        public virtual byte[] Encrypt(byte[] input) {
            return input;
        }
        public virtual byte[] Decrypt(byte[] input) {
            return input;
        }
        #endregion

        protected EncryptionBase() {
            EncryptionKey = string.Empty;
        }

        /// <summary>
        /// Gets the equivalent key for a password, as a string.
        /// </summary>
        /// <param name="password">The password.</param>
        /// <returns>The string equivalent of the encryption key which would be used for the password.</returns>
        public virtual string GetKey(string password) {
            return Encoding.UTF8.GetString(GetKeyBytes(password));
        }

        /// <summary>
        /// Gets the equivalent key for a password.
        /// </summary>
        /// <param name="password">The password.</param>
        /// <returns>The encryption key which would be used for the password.</returns>
        public virtual byte[] GetKeyBytes(string password) {
            return Encoding.UTF8.GetBytes(password);
        }

        /// <summary>
        /// Encrypts an object.
        /// </summary>
        /// <param name="obj">The object.</param>
        /// <returns>The object as an encrypted byte array</returns>
        public virtual byte[] EncryptObject(object obj) {
            if(obj == null) return null;
            try {
                return Encrypt(ByteConversion.ToByteArray(obj));
            } catch {
                return ByteConversion.ToByteArray(obj);
            }
        }

        /// <summary>
        /// Decrypts a given value as type of T, if unsuccessful the defaultValue is used
        /// </summary>
        /// <typeparam name="T">The conversion type</typeparam>
        /// <param name="value">The value to convert</param>
        /// <param name="defaultValue">The default value to return</param>
        /// <returns>The array as an object of the specified type</returns>
        public virtual T DecryptObject<T>(byte[] value, T defaultValue) {
            if(value == null) return defaultValue;

            try {
                Type conversionType = typeof(T);

                // Some trickery for Nullable Types
                if(conversionType.IsGenericType && conversionType.GetGenericTypeDefinition().Equals(typeof(Nullable<>))) {
                    conversionType = new NullableConverter(conversionType).UnderlyingType;
                }
                return (T)ByteConversion.FromByteArray<T>(Decrypt(value));
            } catch {
                // Do nothing
            }
            return defaultValue;
        }

        /// <summary>
        /// Encrypts a string to a byte array.
        /// </summary>
        /// <param name="value">The value to convert</param>
        /// <returns>The encrypted string.</returns>
        public virtual byte[] EncryptString(string value) {
            if(value == null) return null;
            return Encrypt(Encoding.UTF8.GetBytes(value));
        }

        /// <summary>
        /// Decrypts a given value to a string.
        /// </summary>
        /// <param name="value">The value to convert</param>
        /// <returns>The array as a string.</returns>
        public virtual string DecryptString(byte[] value) {
            if(value == null) return null;
            return Encoding.UTF8.GetString(Decrypt(value));
        }


        protected byte[] Encrypt(byte[] input, BufferedBlockCipher bufferedBlockCipher, ICipherParameters cipherParameters) {
            return Process(input, bufferedBlockCipher, cipherParameters, true);
        }

        protected byte[] Decrypt(byte[] input, BufferedBlockCipher bufferedBlockCipher, ICipherParameters cipherParameters) {
            return Process(input, bufferedBlockCipher, cipherParameters, false);
        }

        protected byte[] Process(byte[] input, BufferedBlockCipher bufferedBlockCipher, ICipherParameters cipherParameters, bool forEncryption) {
            bufferedBlockCipher.Init(forEncryption, cipherParameters);

            int inputOffset = 0;
            int inputLength = input.Length;

            int maximumOutputLength = bufferedBlockCipher.GetOutputSize(inputLength);
            byte[] output = new byte[maximumOutputLength];
            int outputOffset = 0;
            int outputLength = 0;

            int bytesProcessed;

            bytesProcessed = bufferedBlockCipher.ProcessBytes(
                    input, inputOffset, inputLength,
                    output, outputOffset
                );
            outputOffset += bytesProcessed;
            outputLength += bytesProcessed;

            bytesProcessed = bufferedBlockCipher.DoFinal(output, outputOffset);
            outputOffset += bytesProcessed;
            outputLength += bytesProcessed;

            if(outputLength == output.Length) {
                return output;
            } else {
                byte[] truncatedOutput = new byte[outputLength];
                Array.Copy(output, 0,
                        truncatedOutput, 0,
                        outputLength
                    );
                return truncatedOutput;
            }
        }

        /// <summary>
        /// Creates an instance of this cipher's padding.
        /// Paddings available (http://www.bouncycastle.org/docs/docs1.6/org/bouncycastle/crypto/paddings/BlockCipherPadding.html):
        ///     - ISO10126d2Padding
        ///     - ISO7816d4Padding
        ///     - PKCS7Padding
        ///     - TBCPadding
        ///     - X923Padding
        ///     - ZeroBytePadding
        /// </summary>
        /// <returns>The appropriate IBlockCipherPadding for this cipher's PaddingMode</returns>
        protected IBlockCipherPadding GetPadding() {
            IBlockCipherPadding padding;
            switch(this.PaddingMode) {
                case PaddingMode.PKCS7:
                    padding = new Pkcs7Padding();
                    break;
                case PaddingMode.ISO10126:
                    padding = new ISO10126d2Padding();
                    break;
                case PaddingMode.Zeros:
                default:
                    padding = new ZeroBytePadding();
                    break;
            }
            return padding;
        }
    }
}
